﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Tesla.Math;

namespace Tesla.Content.Loaders.Model.Dxs {
    public class DxsParser {
        private XmlDocument document;
        private Dictionary<int, DxsMaterial> materialList;

        /// <summary>
        /// Creates a new parser
        /// </summary>
        public DxsParser() {
            document = new XmlDocument();
            materialList = new Dictionary<int, DxsMaterial>();
        }

        public void Reset() {
            materialList.Clear();
            document.RemoveAll();
        }

        /// <summary>
        /// Parse the specified DXS file.
        /// </summary>
        /// <param name="name">Stream containing the dxs file</param>
        /// <returns>Dxs model tree</returns>
        public DxsNode Parse(Stream stream, String name) {
            //Import the XML in document form
            document.Load(stream);
            //Create the root for the model
            DxsNode scene = new DxsNode(Path.GetFileNameWithoutExtension(name));
            //Now parse the children
            XmlNode root = document.FirstChild;
            foreach(XmlNode node in root.ChildNodes) {
                switch(node.Name) {
                    //Materials are listed first and picked up first
                    case "materials":
                        for(int i = 0; i < node.ChildNodes.Count; i++) {
                            ParseMaterials(node.ChildNodes[i]);
                        }
                        break;
                    //All primitives get added to the scene root
                    //TODO: Create nodes for groups?
                    case "primitives":
                        for(int i = 0; i < node.ChildNodes.Count; i++) {
                            ParsePrimitive(node.ChildNodes[i], scene);
                        }
                        break;
                }
            }
            return scene;
        }

        /// <summary>
        /// Parse the specified DXS file.
        /// </summary>
        /// <param name="name">Absolute file path</param>
        /// <returns>Dxs model tree</returns>
        public DxsNode Parse(String name) {
            //Import the XML in document form
            document.Load(name);
            //Create the root for the model
            String s = name.Substring(name.LastIndexOf("\\"));
            DxsNode scene = new DxsNode(s.Substring(0, s.LastIndexOf(".dxs")));
            //Now parse the children
            XmlNode root = document.FirstChild;
            foreach(XmlNode node in root.ChildNodes) {
                switch(node.Name) {
                    //Materials are listed first and picked up first
                    case "materials":
                        for(int i = 0; i < node.ChildNodes.Count; i++) {
                            ParseMaterials(node.ChildNodes[i]);
                        }
                        break;
                    //All primitives get added to the scene root
                    //TODO: Create nodes for groups?
                    case "primitives":
                        for(int i = 0; i < node.ChildNodes.Count; i++) {
                            ParsePrimitive(node.ChildNodes[i], scene);
                        }
                        break;
                }
            }
            return scene;
        }

        /// <summary>
        /// Parse all the material nodes attached to the
        /// specified xml node.
        /// </summary>
        /// <param name="xmlNode">XmlNode that is parent to material nodes</param>
        private void ParseMaterials(XmlNode xmlNode) {
            for(int i = 0; i < xmlNode.ChildNodes.Count; i++) {
                //Get the material node
                XmlNode matNode = xmlNode.ChildNodes[i];
                //Parse attributes
                int id = int.Parse(matNode.Attributes["id"].Value);
                String name = matNode.Attributes["name"].Value;
                bool used = bool.Parse(matNode.Attributes["used"].Value);
                //Only create a new material if its used
                if(used) {
                    DxsMaterial dxsMaterial = new DxsMaterial(name, id);
                    //Parse the layers of the material
                    for(int j = 0; j < matNode.ChildNodes.Count; j++) {
                        XmlNode child = matNode.ChildNodes[j];
                        //Ensure this is a layer node
                        if(child.Name.Equals("layer")) {
                            ParseLayer(child, dxsMaterial);
                        }
                    }
                    //Finally add it to the master list
                    materialList.Add(dxsMaterial.MaterialID, dxsMaterial);
                }
            }
        }

        /// <summary>
        /// Parses a layer node that corresponds to the specified material.
        /// </summary>
        /// <param name="xmlNode">Xml layer node</param>
        /// <param name="dxsMaterial">Material associated with the layer</param>
        private void ParseLayer(XmlNode xmlNode, DxsMaterial dxsMaterial) {
            //Get the material and blend types, this tells us how to parse the data
            String type = xmlNode.Attributes["type"].Value;
            String blend = xmlNode.Attributes["blend"].Value;
            DxsLayer layer = new DxsLayer();

            //First set the blending types
            switch(blend) {
                case "replace":
                    layer.Blend = BlendType.Replace;
                    break;
                case "add":
                    layer.Blend = BlendType.Add;
                    break;
                case "modulate":
                    layer.Blend = BlendType.Modulate;
                    break;
                case "alphablend":
                    layer.Blend = BlendType.Alphablend;
                    break;
                case "inactive":
                    layer.Blend = BlendType.Inactive;
                    break;
            }
            //Next the type of material
            switch(type) {
                case "color":
                    layer.MaterialType = MaterialType.Color;
                    XmlNode child = xmlNode.FirstChild;
                    byte r = byte.Parse(child.Attributes["r"].Value);
                    byte g = byte.Parse(child.Attributes["b"].Value);
                    byte b = byte.Parse(child.Attributes["g"].Value);
                    byte a = byte.Parse(child.Attributes["a"].Value);
                    layer.Color = new Color(r, g, b, a);
                    break;
                case "texture":
                    layer.MaterialType = MaterialType.Texture;
                    child = xmlNode.FirstChild;
                    layer.TextureFile = child.Attributes["file"].Value;
                    break;
                case "lightmap":
                    layer.MaterialType = MaterialType.Lightmap;
                    child = xmlNode.FirstChild;
                    layer.TextureFile = child.Attributes["file"].Value;
                    break;
            }

            dxsMaterial.Layers.Add(layer);
        }

        /// <summary>
        /// Parse a xml primitive node. Result is attached to the specified
        /// Dxs node.
        /// </summary>
        /// <param name="xmlNode">Xml Primitive Node</param>
        /// <param name="dxsNode">Parent Dxs Node</param>
        private void ParsePrimitive(XmlNode xmlNode, DxsNode dxsNode) {
            //Process attributes
            String name = xmlNode.Attributes["name"].Value;
            int groupID = -1;// int.Parse(xmlNode.Attributes["groupID"].Value);
            int skeletonID = -1;// int.Parse(xmlNode.Attributes["skeletonID"].Value);

            //Create a new DXS primitive
            DxsPrimitive prim = new DxsPrimitive(name, groupID, skeletonID);

            //Go through its children - first process the master vertex list,
            //then the polygon list
            foreach(XmlNode node in xmlNode.ChildNodes) {
                switch(node.Name) {
                    case "vertices":
                        for(int i = 0; i < node.ChildNodes.Count; i++) {
                            ParseMasterVertex(node.ChildNodes[i], prim);
                        }
                        break;
                    case "polygons":
                        for(int i = 0; i < node.ChildNodes.Count; i++) {
                            ParsePolygon(node.ChildNodes[i], prim);
                        }
                        break;
                }
            }
            dxsNode.AddChild(prim);
        }

        /// <summary>
        /// Parses a polygon node, which is a vertex list. Each vertex's
        /// position is referenced by its vertex ID to the primitive's master list.
        /// Texture coordinates and lightmap coordinates are picked up here,
        /// as well as triangulation. Polys are added to a primitive grouped by
        /// their material ID so they can be split up later into multiple meshes.
        /// </summary>
        /// <param name="xmlNode">Xml Poly node</param>
        /// <param name="dxsPrim">Dxs Primitive this polygon belongs to</param>
        private void ParsePolygon(XmlNode xmlNode, DxsPrimitive dxsPrim) {
            //Parse attributes
            int mid = int.Parse(xmlNode.Attributes["mid"].Value);
            int count = xmlNode.ChildNodes.Count;
            DxsPoly poly = new DxsPoly(mid);
            //Add the material to the primitive - if its a newly encountered material
            if(materialList.ContainsKey(mid)) {
                dxsPrim.AddMaterial(materialList[mid]);
            }
            //Now go through each vertex and parse it
            for(int i = count - 1; i >= 0; i--) {
           // for(int i = 0; i < count; i++) {
                XmlNode vertex = xmlNode.ChildNodes[i];
                ParseVertex(vertex, dxsPrim, poly);
            }
            //Either triangulate the polygon, or simply add it if its already a triangle
            if(count > 3) {
                TriangulatePolygon(dxsPrim, poly);
            } else {
                poly.ComputeFaceNormal();
                dxsPrim.AddPoly(poly);
            }
        }

        /// <summary>
        /// Uses the first vertex in a polygon as a pivot to triangulate the polygon,
        /// which get added to the dxs primitive.
        /// </summary>
        /// <param name="dxsPrim">Dxs Primitive the poly belongs to</param>
        /// <param name="dxsPoly">Dxs Poly to be triangulated</param>
        private void TriangulatePolygon(DxsPrimitive dxsPrim, DxsPoly dxsPoly) {
            DxsVertex[] vertices = dxsPoly.Vertices.ToArray();
            DxsVertex v0 = vertices[0];
            //Use the first vertex as a pivot and create triangles.
            for(int v = 1; v < vertices.Length - 1; v++) {
                DxsPoly poly = new DxsPoly(dxsPoly.MaterialID);
                poly.Vertices.Add(v0);
                poly.Vertices.Add(vertices[v]);
                poly.Vertices.Add(vertices[v+1]);
                poly.ComputeFaceNormal();
                dxsPrim.AddPoly(poly);
            }
        }

        /// <summary>
        /// Parses a vertex listing in a primitive node, this pertains to the
        /// position data used by the primitive - hence it being the "master list".
        /// All other data such as texture coordinates are referenced by the vertex
        /// ID's to these vertices.
        /// 
        /// Also, if a vertex color is supplied it will be caught here. By default
        /// vertex colors are white, and if a single vertex color is found the 
        /// dxs primitive is enabled to use vertex colors.
        /// </summary>
        /// <param name="xmlNode">Xml primitive node</param>
        /// <param name="dxsPrim">Dxs primitive</param>
        private void ParseMasterVertex(XmlNode xmlNode, DxsPrimitive dxsPrim) {
            //Parse attributes
            int vid = int.Parse(xmlNode.Attributes["id"].Value);
            float x = float.Parse(xmlNode.Attributes["x"].Value);
            float y = float.Parse(xmlNode.Attributes["y"].Value);
            float z = float.Parse(xmlNode.Attributes["z"].Value);
            int jointID = -1;// int.Parse(xmlNode.Attributes["jointID"].Value);
            //create a new dxs vertex
            DxsVertex vertex = new DxsVertex(vid, new Vector3(x, y, z), jointID);
            //See if we have a vertex color
            if(xmlNode.HasChildNodes && xmlNode.LastChild.Name.Equals("color")) {
                XmlNode child = xmlNode.FirstChild;
                byte r = byte.Parse(child.Attributes["r"].Value);
                byte g = byte.Parse(child.Attributes["b"].Value);
                byte b = byte.Parse(child.Attributes["g"].Value);
                byte a = byte.Parse(child.Attributes["a"].Value);
                vertex.VertexColor = new Color(r, g, b, a);
                dxsPrim.VertexColorEnabled = true;
            }

            //Add master vertex to primitive
            dxsPrim.MasterVertexList.Add(vertex);

        }

        /// <summary>
        /// Parses a vertex found in a polygon's vertex list. Parses texture and
        /// lightmap coordinates based on the polygon's material type.
        /// </summary>
        /// <param name="xmlNode">Xml Poly node</param>
        /// <param name="dxsPrim">Dxs Primitive this Poly belongs to</param>
        /// <param name="dxsPoly">Dxs Poly the vertex belongs to</param>
        private void ParseVertex(XmlNode xmlNode, DxsPrimitive dxsPrim, DxsPoly dxsPoly) {
            //Get the vertex ID so we can look up the position info
            int vid = int.Parse(xmlNode.Attributes["vid"].Value);

            //Copy the master vertex (position + vertex color)
            DxsVertex vertex = new DxsVertex(dxsPrim.MasterVertexList[vid]);

            //Get the material of the polygon
            DxsMaterial material = dxsPrim.Materials[dxsPoly.MaterialID];
            //If the first layer is a texture, parse texture coordinates
            if(material.Layers.Count > 0 && material.Layers[0].MaterialType == MaterialType.Texture) {
                float u0 = float.Parse(xmlNode.Attributes["u0"].Value);
                float v0 = float.Parse(xmlNode.Attributes["v0"].Value);
                vertex.TextureCoordinate = new Vector2(u0, v0);
            }
            //If one of the layers (last one) is a lightmap, parse its texture coordinates
            for(int i = 1; i < material.Layers.Count; i++) {
                if(material.Layers[i].MaterialType == MaterialType.Lightmap) {
                    float ui = float.Parse(xmlNode.Attributes["u"+i].Value);
                    float vi = float.Parse(xmlNode.Attributes["v"+i].Value);
                    vertex.LightMapCoordinate = new Vector2(ui, vi);
                    break;
                }
            }

            //Add vertex to the poly
            dxsPoly.Vertices.Add(vertex);
        }
    }
}
